import * as monaco from 'monaco-editor';
import * as lz from 'lz-string';

import templateAudio from './templates/audio';
import templateDefault from './templates/default';
import templateSpritesheet from './templates/spritesheet';
import templateTileset from './templates/tileset';

import './styles.css';

type ExcaliburDevTool = { toggleDebug: () => void };
type GlobalWithDevTool = typeof globalThis & { ___EXCALIBUR_DEVTOOL?: ExcaliburDevTool };

const templates: Record<string, string> = {
  audio: templateAudio,
  default: templateDefault,
  spritesheet: templateSpritesheet,
  tileset: templateTileset
};

const isLightMode = window.matchMedia('(prefers-color-scheme: light)').matches;

const searchParams = new URLSearchParams(document.location.search);
const isEmbedded = searchParams.get('embed') === 'true';
const isAutoplay = searchParams.get('autoplay') === 'true';
const template = templates[searchParams.get('template') ?? ''] ?? templates.default;

document.body.classList.toggle('embedded', isEmbedded);

// Super secret worker url
//@ts-ignore
import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker.js?worker';

const getInitialCode = () => {
  const paramsString = window.location.search;
  const searchParams = new URLSearchParams(paramsString);
  const sharedCode = searchParams.get('code');
  const code = sharedCode ? lz.decompressFromEncodedURIComponent(sharedCode) : template;

  return code;
};

// Solution: Configure Monaco Environment before importing
window.MonacoEnvironment = {
  getWorker: (_moduleId: string, label: string) => {
    switch (label) {
      case 'typescript':
      case 'javascript':
        return new TsWorker();
      default:
        return new TsWorker();
    }
  }
} as any;

import exTypes from '../types/index.d.mts?raw';
monaco.languages.typescript.typescriptDefaults.addExtraLib(exTypes, 'file:///index.d.ts');

// Check if TypeScript language server is working
const tsDefaults = monaco.languages.typescript.typescriptDefaults;

monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
  ...tsDefaults.getCompilerOptions(),
  paths: {
    excalibur: ['file:///index.d.ts']
  }
});

// These should return the default configurations
// console.log('Compiler Options:', tsDefaults.getCompilerOptions());
// console.log('Diagnostics Options:', tsDefaults.getDiagnosticsOptions());

const containerEl = document.getElementById('container')!;
const autoSaveEl = document.getElementById('auto-save')! as HTMLInputElement;

const editor = monaco.editor.create(containerEl, {
  value: getInitialCode(),
  language: 'typescript',
  automaticLayout: true,
  theme: isLightMode ? 'vs-light' : 'vs-dark',
  minimap: { enabled: !isEmbedded }
});

function debounce(func: (..._: any[]) => any, delay: number) {
  let timeout: number;
  return function (this: unknown, ...args: any[]) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, delay) as unknown as number;
  };
}

const saveHandler = debounce(async (e) => {
  if (autoSaveEl.checked) {
    await shareCode(false);
  }

  // Example: Handle Ctrl+S
  if (e.ctrlKey && e.keyCode === monaco.KeyCode.KeyS) {
    e.preventDefault();
    await shareCode(false);
  }
}, 500);

editor.onKeyDown(saveHandler);

/**
 * esm tagged template literal from Dr. Axel
 * https://2ality.com/2019/10/eval-via-import.html
 */
function esm(templateStrings: TemplateStringsArray, ...substitutions: any[]) {
  let js = templateStrings.raw[0];
  for (let i = 0; i < substitutions.length; i++) {
    js += substitutions[i] + templateStrings.raw[i + 1];
  }
  return 'data:text/javascript;base64,' + btoa(js);
}

const buildButtonEl = document.getElementById('build')! as HTMLButtonElement;
const debugButtonEl = document.getElementById('debug')! as HTMLButtonElement;
const shareButtonEl = document.getElementById('share')! as HTMLButtonElement;
const loadingEl = document.getElementsByClassName('loading')[0]! as HTMLDivElement;

const getClient = async (model: monaco.editor.ITextModel) => {
  const maxAttempts = 3;
  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const getWorker = await monaco.languages.typescript.getTypeScriptWorker();
      const client = await getWorker(model.uri);
      return client;
    } catch (err) {
      lastError = err;
      const message = String(err);
      const shouldRetry = message.includes('TypeScript not registered') && attempt < maxAttempts;
      // eslint-disable-next-line no-console
      console.warn(`Error getting TypeScript worker client (attempt ${attempt}/${maxAttempts}):`, err);
      if (!shouldRetry) {
        throw err;
      }

      // A little exponential backoff never hurt anyone
      // @see https://github.com/microsoft/monaco-editor/issues/115
      await new Promise((res) => setTimeout(res, Math.pow(2, attempt) * 100));
    }
  }

  throw lastError;
};

const buildAndRun = async () => {
  loadingEl.style.display = 'block';
  const model = editor.getModel()!;
  const client = await getClient(model);
  const runnanbleJs = await client.getEmitOutput(model.uri.toString(), false, false);
  const firstJs = runnanbleJs.outputFiles.find((f) => f.name.endsWith('.js'));
  if (firstJs) {
    // Dr. Axel to the rescue
    // https://2ality.com/2019/10/eval-via-import.html
    try {
      await import(/* @vite-ignore */ esm`${firstJs.text}`);
    } finally {
      loadingEl.style.display = 'none';
    }
  }
};

const toggleDebug = () => {
  const devTool = (globalThis as GlobalWithDevTool).___EXCALIBUR_DEVTOOL;
  devTool?.toggleDebug();
};

const shareCode = async (writeToClipboard?: boolean) => {
  const code = editor.getModel()?.getValue() ?? '';
  const encoded = `code=${lz.compressToEncodedURIComponent(code)}`;
  const url = `${window.location}?${encoded}`;
  if (writeToClipboard) {
    await navigator.clipboard.writeText(url);
  }
  window.history.pushState({}, '', '?' + encoded);
};
shareButtonEl.addEventListener('click', () => shareCode(true));
debugButtonEl.addEventListener('click', toggleDebug);
buildButtonEl.addEventListener('click', buildAndRun);

if (isAutoplay) {
  void buildAndRun();
}

window.addEventListener('keydown', (evt: KeyboardEvent) => {
  if (evt.code === 'Escape') {
    evt.preventDefault();
    buildButtonEl.focus();
    return false;
  }
  if ((evt.ctrlKey || evt.metaKey) && evt.code === 'KeyS') {
    evt.preventDefault();
    void buildAndRun();
    return false;
  }

  if ((evt.ctrlKey || evt.metaKey) && evt.code === 'KeyD') {
    evt.preventDefault();
    toggleDebug();
    return false;
  }
  return true;
});
